﻿// <copyright file="CommunityBase.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-04-13</date>
// <summary>New York Times Community API class definition</summary>

namespace NYTimes.Core
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Windows;
#if SILVERLIGHT && WINDOWS_PHONE
#elif SILVERLIGHT
    using System.Windows.Browser;
#endif
    using System.Windows.Media.Imaging;
    using System.Xml.Linq;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// Comment sorting
    /// </summary>
    public enum CommentSortType
    {
        /// <summary>
        /// Sort by newest comment first
        /// </summary>
        Newest,

        /// <summary>
        /// Sort by oldest comment first
        /// </summary>
        Oldest,

        /// <summary>
        /// Sort by recommended comments first
        /// </summary>
        Recommended,

        /// <summary>
        /// Replied comments first
        /// </summary>
        Replied,

        /// <summary>
        /// Editor's selection first
        /// </summary>
        EditorsSelection
    }

    /// <summary>
    /// New York Times Community API Silverlight CLR Base class
    /// </summary>
    /// <remarks>See <a href="http://developer.nytimes.com/docs/community_api" target="_blank">
    /// this article</a> for
    /// more information about the Community API.
    /// <list type="table">
    /// <listheader>
    /// <term>Rate Limiting</term>
    /// </listheader>
    ///     <item><term>Default Queries Per Second</term><description>30</description></item>
    ///     <item><term>Resource Key</term><description>NYTCommunityKey</description></item>
    ///     <item><term>Overriding default QPS</term><description>To Override the rate limiting add 
    ///     a namespace declaration <c>xmlns:sys="clr-namespace:System;assembly=mscorlib"</c> and 
    ///     an integer resource to the App.Xaml Application.Resources like this: 
    ///     <c>&lt;sys:Int32 x:Key="NYTCommunityKey"&gt;40&lt;/sys:Int32&gt;</c></description>
    ///     </item>
    /// </list>
    /// </remarks>
    public abstract class CommunityBase : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// The Application Resource key for the NYT Community API key
        /// </summary>
        private const string NYTCommunityKey = "NYTCommunityKey";

        /// <summary>
        ///  The API Version
        /// </summary>
        private const string ApiVersion = "v2";

        /// <summary>
        /// the base URI
        /// </summary>
        private const string BaseUri = "http://api.nytimes.com/svc/community/{0}/comments/";

        /// <summary>
        /// 30 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTCommunity", 30);

        /// <summary>
        /// the web client
        /// </summary>
        private WebClient client;

        /// <summary>
        /// the API Key
        /// </summary>
        private string apiKey;

        /// <summary>
        /// the Copyright text
        /// </summary>
        private string copyright;

        /// <summary>
        /// the total number of comments
        /// </summary>
        private int totalComments;

        /// <summary>
        /// true if the results are loading
        /// </summary>
        private bool isLoading;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the CommunityBase class.
        /// </summary>
        protected CommunityBase()
        {
            if (Application.Current != null && Application.Current.Resources.Contains(NYTCommunityKey))
            {
                this.ApiKey = Application.Current.Resources[NYTCommunityKey] as string;
            }

            this.Results = new ObservableCollection<Comment>();
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event handler
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the API key
        /// </summary>
        [Description("The New York Times Community API Key")]
        public string ApiKey 
        { 
            get
            {
                return this.apiKey;
            }

            set
            {
                this.apiKey = value;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the results are loading
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    this.TriggerPropertyChanged("IsLoading");
                }
            }
        }

        /// <summary>
        /// Gets the comments returned as an observable collection of 
        /// <see cref="NYTimes.Data.Comment"/> objects.
        /// </summary>
        public ObservableCollection<Comment> Results { get; private set; }

        /// <summary>
        /// Gets the logo for the New York Times
        /// </summary>
        [Description("The New York Times Logo Image"),
         SuppressMessage("Microsoft.Performance", "CA1822", 
            Justification = "This is exposed as a property to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var uri = new Uri(
                    "NYTimesSilverlightKit;component/Images/Logo.png", 
                    UriKind.Relative);

                var streamResource = Application.GetResourceStream(uri);

                var image = new BitmapImage();

#if SILVERLIGHT
                image.SetSource(streamResource.Stream);
#else
                image.StreamSource = streamResource.Stream;
#endif

                return image;
            }
        }

        /// <summary>
        /// Gets or sets the total number of comments
        /// </summary>
        public int TotalComments 
        { 
            get
            {
                return this.totalComments;
            }

            protected set
            {
                if (this.totalComments != value)
                {
                    this.totalComments = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("TotalComments"));
                    }
                }
            } 
        }

        /// <summary>
        /// Gets the copyright text
        /// </summary>
        public string Copyright 
        {
            get
            {
                return this.copyright;
            }

            private set
            {
                this.copyright = value;

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("Copyright"));
                }
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Generate the URI string
        /// </summary>
        /// <returns>the output URI string</returns>
        [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "This is used to create URI fragments.")]
        public abstract string GenerateUriFragment();

        /// <summary>
        /// Fetch the next set (if supported)
        /// </summary>
        /// <returns>true if the results are done loading.</returns>
        public virtual bool FetchNextSet()
        {
            return true;
        }

        /// <summary>
        /// Override to parse the result set
        /// </summary>
        /// <param name="resultSet">the result set</param>
        public virtual void Parse(XElement resultSet)
        {
        }

        /// <summary>
        /// Append the sort type
        /// </summary>
        /// <param name="builder">the string builder</param>
        /// <param name="sortType">the sort type</param>
        protected static void AppendSortType(StringBuilder builder, CommentSortType sortType)
        {
            if (builder == null)
            {
                throw new ArgumentNullException("builder");
            }

            switch (sortType)
            {
                case CommentSortType.EditorsSelection:
                    builder.Append("sort=editors-selection");
                    break;
                case CommentSortType.Newest:
                    builder.Append("sort=newest");
                    break;
                case CommentSortType.Oldest:
                    builder.Append("sort=oldest");
                    break;
                case CommentSortType.Recommended:
                    builder.Append("sort=recommended");
                    break;
                case CommentSortType.Replied:
                    builder.Append("sort=replied");
                    break;
            }
        }

        /// <summary>
        /// Trigger the Property changed event handler(s)
        /// </summary>
        /// <param name="propertyName">the property name</param>
        protected void TriggerPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Fetch the results
        /// </summary>
        /// <param name="clear">true to clear the result set first</param>
        protected void FetchResults(bool clear)
        {
            if (SampleData.IsInDesignTool)
            {
                return;
            }

            if (clear)
            {
                this.Results.Clear();

                this.IsLoading = true;
            }

            if (this.client != null)
            {
                this.client.CancelAsync();

                limiting.Cancel(this.client);

                this.client = null;
            }

            var uri = this.GenerateUri();

            if (uri == null)
            {
                return;
            }

            System.Diagnostics.Debug.WriteLine(uri);

            this.client = new WebClient();

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnCommentsReturned);

            limiting.MakeWebRequest(this.client, uri);
        }

        /// <summary>
        /// Parse the XML
        /// </summary>
        /// <param name="xml">the XML returned from the service</param>
        protected void ParseXml(string xml)
        {
            var doc = XDocument.Parse(xml);

            var resultSet = doc.Element("result_set");

            this.Copyright = resultSet.Element("copyright").Value;

            var resultsElement = resultSet.Element("results");

            if (resultsElement.Element("totalCommentsFound") != null)
            {
                this.TotalComments = int.Parse(resultsElement.Element("totalCommentsFound").Value, CultureInfo.InvariantCulture);
            }
            else
            {
                this.TotalComments = int.MaxValue;
            }

            var commentsElement = resultsElement.Element("comments");

            if (commentsElement != null)
            {
                var comments = from comment in commentsElement.Elements("comment")
                               select CreateComment(comment);

                foreach (var comment in comments)
                {
                    this.Results.Add(comment);
                }
            }

            this.Parse(resultSet);
        }
        #endregion

        #region Implementation
        /// <summary>
        /// Create a comment from an XML Element
        /// </summary>
        /// <param name="comment">the XML element</param>
        /// <returns>a new comment</returns>
        private static Comment CreateComment(XElement comment)
        {
            var replies = comment.Element("replies");

            XElement comments = null;

            if (replies != null)
            {
                comments = comment.Element("replies").Element("comments");
            }

            return new Comment()
            {
                ArticleUrl = comment.Element("articleURL") == null ? null : new Uri(comment.Element("articleURL").Value, UriKind.Absolute),               
                ApproveDate = Synergist.Utility.UnixTimeToDateTime(comment.Element("approveDate")).Value,
                Body = HtmlDecodeElement(comment.Element("commentBody")),
                CommentQuestion = comment.Element("commentQuestion") == null ? null : comment.Element("commentQuestion").Value,
                CreateDate = Synergist.Utility.UnixTimeToDateTime(comment.Element("createDate")),
                DisplayName = comment.Element("display_name") == null ? null : comment.Element("display_name").Value,
                Location = comment.Element("location") == null ? null : comment.Element("location").Value,
                Recommendations = comment.Element("recommendations") == null ? 0 : int.Parse(comment.Element("recommendations").Value, CultureInfo.InvariantCulture),
                Replies = comments == null ? null : new List<Comment>(from reply in comments.Elements("comment")
                          select CreateComment(reply)),
                Sharing = comment.Element("sharing") == null ? 0 : string.IsNullOrEmpty(comment.Element("sharing").Value) ? 0 : int.Parse(comment.Element("sharing").Value, CultureInfo.InvariantCulture),
                TimesPeople = comment.Element("times_people") == null ? 0 : int.Parse(comment.Element("times_people").Value, CultureInfo.InvariantCulture),
                Title = comment.Element("commentTitle").Value,
                UpdateDate = Synergist.Utility.UnixTimeToDateTime(comment.Element("updateDate")),
                UserComments = comment.Element("userComments") == null ? null : CreateUserCommentsUri(comment.Element("userComments").Value),
                UserTitle = comment.Element("userTitle") == null ? null : comment.Element("userTitle").Value,
                UserUrl = comment.Element("userURL") == null ? null : string.IsNullOrEmpty(comment.Element("userURL").Value) ? null : new Uri(comment.Element("userURL").Value, UriKind.Absolute)
            };
        }

        /// <summary>
        /// HTML Decode
        /// </summary>
        /// <param name="htmlContent">an XML Element with encoded HTML content</param>
        /// <returns>the decoded HTML with linebreaks turned into newline characters</returns>
        private static string HtmlDecodeElement(XElement htmlContent)
        {
            var text = htmlContent.Value;

#if SILVERLIGHT
            var decoded = HttpUtility.HtmlDecode(text);
#else
            var decoded = WebUtility.HtmlDecode(text);
#endif

            return decoded.Replace("<br />", "\n");
        }

        /// <summary>
        /// Create a user comment URI
        /// </summary>
        /// <param name="userComments">the user comment text</param>
        /// <returns>a service URI</returns>
        private static Uri CreateUserCommentsUri(string userComments)
        {
            Uri userCommentsUri;
            if (userComments.StartsWith("/", StringComparison.OrdinalIgnoreCase))
            {
                userCommentsUri = new Uri(string.Format(CultureInfo.InvariantCulture, "http://api.nytimes.com{0}", userComments), UriKind.Absolute);
            }
            else
            {
                userCommentsUri = new Uri(string.Format(CultureInfo.InvariantCulture, "http://{0}", userComments), UriKind.Absolute);
            }

            return userCommentsUri;
        }

        /// <summary>
        /// Generate the URI
        /// </summary>
        /// <returns>the URI for the service call</returns>
        private Uri GenerateUri()
        {
            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return null;
            }

            var fragment = this.GenerateUriFragment();

            if (fragment == null)
            {
                return null;
            }

            var uriString = string.Format(CultureInfo.InvariantCulture, BaseUri, ApiVersion) + fragment;

            var builder = new StringBuilder(uriString);

            builder.AppendFormat(CultureInfo.InvariantCulture, "&api-key={0}", this.ApiKey);

            return new Uri(builder.ToString(), UriKind.Absolute);
        }

        /// <summary>
        /// Comments returned event handler
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnCommentsReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            this.client = null;

            if (e.Error != null)
            {
                if (!e.Cancelled)
                {
                    System.Diagnostics.Debug.WriteLine(e.Error.Message);

                    this.IsLoading = false;
                }

                return;
            }

            this.ParseXml(e.Result);

            if (this.FetchNextSet())
            {
                this.IsLoading = false;
            }
        }

        #endregion
    }
}
